﻿// <copyright>
// Copyright (c) 2008 - 2009, Microsoft, All Rights Reserved
// </copyright>
//
// Licensed under the Microsoft Public License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at 
//
// http://www.opensource.org/licenses/ms-pl.html

namespace LiveLabs.Logging
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using LiveLabs.Logging.Logs;

    /// <summary>
    /// Maintains settings for a group of Loggers
    /// </summary>
    /// <remarks>
    /// <para>
    /// Maintains LogLevels, formatters, and a default Log implementation.  Unless the method 
    /// documentation says differently, all settings are hierarchical.  For example, if you
    /// set the LogLevel of a logger name "a.b.c" to Fatal, all children of "a.b.c" will use that
    /// LogLevel.  Children include any logger name that starts with "a.b.c".  So "a.b.c.d",
    /// "a.b.c.e", etc. would pick up the Fatal level.  But "a.b.f", would not.
    /// </para>
    /// <para>This class is thread safe for multiple readers, but not multiple writers.</para>
    /// </remarks>
    public class Settings
    {
        private readonly Dictionary<LoggerName, LogLevel> nameToLevelMapping
            = new Dictionary<LoggerName, LogLevel>();

        private readonly Dictionary<LoggerName, MessageFormatter> nameToMessageFormatterMapping
            = new Dictionary<LoggerName, MessageFormatter>();

        private readonly Dictionary<LoggerName, ExceptionFormatter> nameToExceptionFormatterMapping
            = new Dictionary<LoggerName, ExceptionFormatter>();

        private readonly Dictionary<LoggerName, Log> nameToLogMapping
            = new Dictionary<LoggerName, Log>();

        private LogLevel defaultLevel = LogLevel.Inform;

        private Log defaultLog = StdErrLog.Instance;

        /// <summary>
        /// Gets or sets the default <see cref="Settings"/> instance for the current AppDomain
        /// </summary>
        public static Settings Default
        {
            get 
            { 
                return DefaultSettingsHolder.Default; 
            }

            set 
            {
                if (value == null)
                {
                    throw new ArgumentNullException("value");
                }

                DefaultSettingsHolder.Default = value; 
            }
        }

        /// <summary>
        /// Gets or sets the default <see cref="LogLevel"/>
        /// </summary>
        /// <remarks>
        /// This <see cref="LogLevel"/> is used when no specific level is registered for a log name.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">If value is not a valid LogLevel</exception>
        /// <seealso cref="LogLevel"/>
        public LogLevel DefaultLogLevel
        {
            get { return this.defaultLevel; }
            set { this.defaultLevel = value.Valid("value"); }
        }

        /// <summary>
        /// Gets or sets the default <see cref="Log"/>.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This <see cref="Log"/> is used when no specific level is registered for a log name.
        /// </para>
        /// <para>
        /// This property will always be set.  If you try to set to null, it will reset
        /// itself to the <see cref="StdErrLog"/>.
        /// </para>
        /// </remarks>
        /// <seealso cref="Log"/>
        public Log DefaultLog
        {
            get
            {
                return this.defaultLog;
            }

            set
            {
                if (value == null)
                {
                    value = StdErrLog.Instance;
                }

                this.defaultLog = value;
            }
        }

        /// <summary>
        /// Finds a <see cref="LogLevel"/> for a logger with the specified name. 
        /// </summary>
        /// <remarks>
        /// <see cref="DefaultLogLevel"/> is returned if the logger name is not registered with this instance.
        /// </remarks>
        /// <param name="name">The name of the logger to find a <see cref="LogLevel"/> for.</param>
        /// <returns>The registered <see cref="LogLevel"/>, or the <see cref="DefaultLogLevel"/></returns>
        public LogLevel FindLogLevelFor(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            return this.FindLogLevelFor(new LoggerName(name));
        }

        /// <summary>
        /// Finds a <see cref="LogLevel"/> for the specified type. 
        /// </summary>
        /// <remarks>
        /// <see cref="DefaultLogLevel"/> is returned if the type is not registered with this instance.
        /// </remarks>
        /// <param name="type">The Type to find a <see cref="LogLevel"/> for.</param>
        /// <returns>The registered <see cref="LogLevel"/>, or the <see cref="DefaultLogLevel"/></returns>
        public LogLevel FindLogLevelFor(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            return this.FindLogLevelFor(new LoggerName(type));
        }

        /// <summary>
        /// Finds a <see cref="LogLevel"/> for the specified type. 
        /// </summary>
        /// <remarks>
        /// <see cref="DefaultLogLevel"/> is returned if the type is not registered with this instance.
        /// </remarks>
        /// <typeparam name="T">The Type to find a <see cref="LogLevel"/> for.</typeparam>
        /// <returns>The registered <see cref="LogLevel"/>, or the <see cref="DefaultLogLevel"/></returns>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Generic method does have non generic counterpart")]
        public LogLevel FindLogLevelFor<T>()
        {
            return this.FindLogLevelFor(new LoggerName(typeof(T)));
        }

        /// <summary>
        /// Registers a <see cref="LogLevel"/> for a logger with the specified name.
        /// </summary>
        /// <param name="name">The name of the logger to register</param>
        /// <param name="logLevel">The level to give the logger</param>
        /// <exception cref="ArgumentNullException">If <paramref name="loggerName"/> is null or empty</exception>
        /// <exception cref="ArgumentOutOfRangeException">If <paramref name="logLevel"/> is not a valid value.</exception>
        public void RegisterLogLevelFor(string name, LogLevel logLevel)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            this.RegisterLogLevelFor(new LoggerName(name), logLevel);
        }

        /// <summary>
        /// Registers a <see cref="LogLevel"/> for the specified type. 
        /// </summary>
        /// <param name="type">The type to register</param>
        /// <param name="logLevel">The level to give the logger</param>
        /// <exception cref="ArgumentNullException">If <paramref name="type"/> is null or empty</exception>
        /// <exception cref="ArgumentOutOfRangeException">If <paramref name="logLevel"/> is not a valid value.</exception>
        public void RegisterLogLevelFor(Type type, LogLevel logLevel)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            this.RegisterLogLevelFor(new LoggerName(type), logLevel);
        }

        /// <summary>
        /// Registers a <see cref="LogLevel"/> for the specified type. 
        /// </summary>
        /// <typeparam name="T">The type to register</typeparam>
        /// <param name="logLevel">The level to give the logger</param>
        /// <exception cref="ArgumentOutOfRangeException">If <paramref name="logLevel"/> is not a valid value.</exception>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Generic method does have non generic counterpart")]
        public void RegisterLogLevelFor<T>(LogLevel logLevel)
        {
            this.RegisterLogLevelFor(new LoggerName(typeof(T)), logLevel);
        }

        /// <summary>
        /// Finds a <see cref="MessageFormatter"/> for a logger of the specified name
        /// </summary>
        /// <param name="name">The name of the logger to find the formatter for</param>
        /// <returns>
        /// The registered <see cref="MessageFormatter"/> or the <see cref="DefaultFormatters.Message"/>
        /// </returns>
        /// <seealso cref="DefaultFormatters"/>
        public MessageFormatter FindMessageFormatterFor(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            return this.FindMessageFormatterFor(new LoggerName(name));
        }

        /// <summary>
        /// Finds a <see cref="MessageFormatter"/> for a logger for the specified type
        /// </summary>
        /// <param name="type">The type for a logger to find the formatter for</param>
        /// <returns>
        /// The registered <see cref="MessageFormatter"/> or the <see cref="DefaultFormatters.Message"/>
        /// </returns>
        /// <seealso cref="DefaultFormatters"/>
        public MessageFormatter FindMessageFormatterFor(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            return this.FindMessageFormatterFor(new LoggerName(type));
        }

        /// <summary>
        /// Finds a <see cref="MessageFormatter"/> for a logger for the specified type
        /// </summary>
        /// <typeparam name="T">The type for a logger to find the formatter for</typeparam>
        /// <returns>
        /// The registered <see cref="MessageFormatter"/> or the <see cref="DefaultFormatters.Message"/>
        /// </returns>
        /// <seealso cref="DefaultFormatters"/>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Generic method does have non generic counterpart")]
        public MessageFormatter FindMessageFormatterFor<T>()
        {
            return this.FindMessageFormatterFor(new LoggerName(typeof(T)));
        }

        /// <summary>
        /// Registers a <see cref="MessageFormatter"/> for a logger with the specified name.
        /// </summary>
        /// <param name="name">The name of the logger</param>
        /// <param name="formatter">The <see cref="MessageFormatter"/> to register</param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="name"/> is null or empty, or if <paramref name="formatter"/> is null.
        /// </exception>
        public void RegisterMessageFormatterFor(string name, MessageFormatter formatter)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            this.RegisterMessageFormatterFor(new LoggerName(name), formatter);
        }

        /// <summary>
        /// Registers a <see cref="MessageFormatter"/> for a logger for the specified type.
        /// </summary>
        /// <param name="type">The <see cref="System.Type"/> the logger was created for</param>
        /// <param name="formatter">The <see cref="MessageFormatter"/> to register</param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="type"/> is null or empty, or if <paramref name="formatter"/> is null.
        /// </exception>
        public void RegisterMessageFormatterFor(Type type, MessageFormatter formatter)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            this.RegisterMessageFormatterFor(new LoggerName(type), formatter);
        }

        /// <summary>
        /// Registers a <see cref="MessageFormatter"/> for a logger for the specified type.
        /// </summary>
        /// <typeparam name="T">The <see cref="System.Type"/> the logger was created for</typeparam>
        /// <param name="formatter">The <see cref="MessageFormatter"/> to register</param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="formatter"/> is null.
        /// </exception>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Generic method does have non generic counterpart")]
        public void RegisterMessageFormatterFor<T>(MessageFormatter formatter)
        {
            this.RegisterMessageFormatterFor(new LoggerName(typeof(T)), formatter);
        }

        /// <summary>
        /// Finds an <see cref="ExceptionFormatter"/> for a logger of the specified name
        /// </summary>
        /// <param name="name">The name of the logger to find the formatter for</param>
        /// <returns>
        /// The registered <see cref="ExceptionFormatter"/> or the <see cref="DefaultFormatters.Exception"/>
        /// </returns>
        /// <seealso cref="DefaultFormatters"/>
        public ExceptionFormatter FindExceptionFormatterFor(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            return this.FindExceptionFormatterFor(new LoggerName(name));
        }

        /// <summary>
        /// Finds a <see cref="ExceptionFormatter"/> for a logger for the specified type
        /// </summary>
        /// <param name="type">The type for a logger to find the formatter for</param>
        /// <returns>
        /// The registered <see cref="ExceptionFormatter"/> or the <see cref="DefaultFormatters.Exception"/>
        /// </returns>
        /// <seealso cref="DefaultFormatters"/>
        public ExceptionFormatter FindExceptionFormatterFor(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            return this.FindExceptionFormatterFor(new LoggerName(type));
        }

        /// <summary>
        /// Finds a <see cref="ExceptionFormatter"/> for a logger for the specified type
        /// </summary>
        /// <typeparam name="T">The type for a logger to find the formatter for</typeparam>
        /// <returns>
        /// The registered <see cref="ExceptionFormatter"/> or the <see cref="DefaultFormatters.Exception"/>
        /// </returns>
        /// <seealso cref="DefaultFormatters"/>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Generic method does have non generic counterpart")]
        public ExceptionFormatter FindExceptionFormatterFor<T>()
        {
            return this.FindExceptionFormatterFor(new LoggerName(typeof(T)));
        }

        /// <summary>
        /// Registers a <see cref="ExceptionFormatter"/> for a logger with the specified name.
        /// </summary>
        /// <param name="name">The name of the logger</param>
        /// <param name="formatter">The <see cref="ExceptionFormatter"/> to register</param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="name"/> is null or empty, or if <paramref name="formatter"/> is null.
        /// </exception>
        public void RegisterExceptionFormatterFor(string name, ExceptionFormatter formatter)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            this.RegisterExceptionFormatterFor(new LoggerName(name), formatter);
        }

        /// <summary>
        /// Registers a <see cref="ExceptionFormatter"/> for a logger for the specified type.
        /// </summary>
        /// <param name="type">The <see cref="System.Type"/> the logger was created for</param>
        /// <param name="formatter">The <see cref="ExceptionFormatter"/> to register</param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="type"/> is null or empty, or if <paramref name="formatter"/> is null.
        /// </exception>
        public void RegisterExceptionFormatterFor(Type type, ExceptionFormatter formatter)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            this.RegisterExceptionFormatterFor(new LoggerName(type), formatter);
        }

        /// <summary>
        /// Registers a <see cref="ExceptionFormatter"/> for a logger for the specified type.
        /// </summary>
        /// <typeparam name="T">The <see cref="System.Type"/> the logger was created for</typeparam>
        /// <param name="formatter">The <see cref="ExceptionFormatter"/> to register</param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="formatter"/> is null.
        /// </exception>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Generic method does have non generic counterpart")]
        public void RegisterExceptionFormatterFor<T>(ExceptionFormatter formatter)
        {
            this.RegisterExceptionFormatterFor(new LoggerName(typeof(T)), formatter);
        }

        /// <summary>
        /// Finds a <see cref="Log"/> implementation for the logger with the specified name.
        /// </summary>
        /// <param name="name">The name of the logger to find a <see cref="Log"/> for.</param>
        /// <returns>A registered <see cref="Log"/> or the <see cref="DefaultLog"/>.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="name"/> is null or empty.</exception>
        public Log FindLogFor(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            return this.FindLogFor(new LoggerName(name));
        }

        /// <summary>
        /// Finds a <see cref="Log"/> implementation for a logger for the specified type.
        /// </summary>
        /// <param name="type">The <see cref="System.Type"/> the logger was created for.</param>
        /// <returns>A registered <see cref="Log"/> or the <see cref="DefaultLog"/>.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="type"/> is null.</exception>
        public Log FindLogFor(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            return this.FindLogFor(new LoggerName(type));
        }

        /// <summary>
        /// Finds a <see cref="Log"/> implementation for a logger for the specified type.
        /// </summary>
        /// <typeparam name="T">The <see cref="System.Type"/> the logger was created for.</typeparam>
        /// <returns>A registered <see cref="Log"/> or the <see cref="DefaultLog"/>.</returns>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Generic method does have non generic counterpart")]
        public Log FindLogFor<T>()
        {
            return this.FindLogFor(new LoggerName(typeof(T)));
        }

        /// <summary>
        /// Registers a <see cref="Log"/> for a logger with the specified name.
        /// </summary>
        /// <param name="name">The name of the logger to register for.</param>
        /// <param name="log">The <see cref="Log"/> to register</param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="name"/> is null or empty.  Or if <paramref name="log"/> is null.
        /// </exception>
        public void RegisterLogFor(string name, Log log)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            this.RegisterLogFor(new LoggerName(name), log);
        }

        /// <summary>
        /// Registers a <see cref="Log"/> for a logger for the specified type.
        /// </summary>
        /// <param name="type">The <see cref="System.Type"/> the logger was created from.</param>
        /// <param name="log">The <see cref="Log"/> to register</param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="type"/> is null.  Or if <paramref name="log"/> is null.
        /// </exception>
        public void RegisterLogFor(Type type, Log log)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            this.RegisterLogFor(new LoggerName(type), log);
        }

        /// <summary>
        /// Registers a <see cref="Log"/> for a logger for the specified type.
        /// </summary>
        /// <typeparam name="T">The <see cref="System.Type"/> the logger was created from.</typeparam>
        /// <param name="log">The <see cref="Log"/> to register</param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="log"/> is null.
        /// </exception>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Generic method does have non generic counterpart")]
        public void RegisterLogFor<T>(Log log)
        {
            this.RegisterLogFor(new LoggerName(typeof(T)), log);
        }

        internal LogLevel FindLogLevelFor(LoggerName name)
        {
            return FindValueFor(name, this.nameToLevelMapping, this.defaultLevel);
        }

        internal MessageFormatter FindMessageFormatterFor(LoggerName name)
        {
            return FindValueFor(name, this.nameToMessageFormatterMapping, DefaultFormatters.Message);
        }

        internal ExceptionFormatter FindExceptionFormatterFor(LoggerName name)
        {
            return FindValueFor(name, this.nameToExceptionFormatterMapping, DefaultFormatters.Exception);
        }

        internal Log FindLogFor(LoggerName name)
        {
            return FindValueFor(name, this.nameToLogMapping, this.defaultLog);
        }

        private static TValue FindValueFor<TValue>(
            LoggerName name, Dictionary<LoggerName, TValue> valuesDictionary, TValue defaultValue)
        {
            Debug.Assert(name != null, "name must not be null");

            TValue result = defaultValue;
            if (valuesDictionary.Count > 0)
            {
                foreach (LoggerName hierarchyLevel in name.Hierarchy)
                {
                    // use a temp here since TryGetValue sets the out param, to default(TValue).
                    // And that may or may not be the same as the passed in defaultValue
                    TValue getValueResult;
                    if (valuesDictionary.TryGetValue(hierarchyLevel, out getValueResult))
                    {
                        result = getValueResult;
                        break;
                    }
                }
            }

            return result;
        }

        private static void RegisterValueFor<TValue>(LoggerName name, Dictionary<LoggerName, TValue> valuesDictionary, TValue value)
        {
            Debug.Assert(name != null, "name cannot be null");

            // use indexer so that values can be overwritten
            valuesDictionary[name] = value;
        }

        private void RegisterLogLevelFor(LoggerName name, LogLevel level)
        {
            level.Valid("level");
            RegisterValueFor(name, this.nameToLevelMapping, level);
        }

        private void RegisterMessageFormatterFor(LoggerName name, MessageFormatter formatter)
        {
            if (formatter == null)
            {
                throw new ArgumentNullException("formatter");
            }

            RegisterValueFor(name, this.nameToMessageFormatterMapping, formatter);
        }

        private void RegisterExceptionFormatterFor(LoggerName name, ExceptionFormatter formatter)
        {
            if (formatter == null)
            {
                throw new ArgumentNullException("formatter");
            }

            RegisterValueFor(name, this.nameToExceptionFormatterMapping, formatter);
        }

        private void RegisterLogFor(LoggerName name, Log log)
        {
            if (log == null)
            {
                throw new ArgumentNullException("log");
            }

            RegisterValueFor(name, this.nameToLogMapping, log);
        }

        /// <summary>
        /// Holds our default Settings instance
        /// </summary>
        private static class DefaultSettingsHolder
        {
            private static Settings @default = new Settings();

            internal static Settings Default
            {
                get { return @default; }
                set { @default = value; }
            }
        }
    }
}
